Charlie Calvert's C++ Builder Unleashed
- 15 -
Working with the
Local InterBase Server
Overview
BCB ships with the Local InterBase Server, which is sometimes simply called LIBS.
This tool provides all the capabilities of the full InterBase server, but it runs
on a local machine. You do not need to be connected to a network to be able to run
the Local InterBase Server.
The client software you get with BCB will talk to either LIBS or the standard
version of the InterBase Server. From your point of view as a programmer, you will
find no difference at all between talking to LIBS and talking to an InterBase server
across a network. The only way to tell which server you're connected to is by examining
the path in your current alias. In short, LIBS is the perfect tool for learning or
practicing real client/server database programming even if you're not connected to
a LAN.
The goal of this chapter is to provide you with a useful introduction to LIBS
and also a brief overview of transactions. In particular, you will see how to do
the following:
- Connect to local InterBase tables
- Connect without having to specify a password
- Create databases
- Work with TDatabase objects
- Create tables
- Commit and roll back transactions in both local and InterBase tables
- Maintain the data you have created
- Work with cached updates
- Create many-to-many relationships
Everything that is said about the local InterBase in this chapter applies equally
to the full server version of InterBase. As a result, this chapter will also be of
interest to people who use InterBase on a network. In particular, this chapter shows
how you can use a local system to create a database that is fully compatible with
the network version of InterBase. To convert a LIBS database to a real client/server
application on a network, you just have to copy your database onto another machine:
copy MyDatabase.gdb p:\remote\nt\drive
You just copy the one file onto the network. No other steps are necessary, other
than changing the path in your alias. Of course, you will also need a real copy of
the InterBase server.
Note that some versions of BCB ship with at least two licenses for the InterBase
server. The real InterBase server runs on most platforms, including Windows 95, Windows
NT, and a wide range of UNIX platforms.
Many readers of this book will come from the world of "big iron," where
the only kinds of databases that exist are servers such as Oracle, Sybase, InterBase,
AS400, or DB2. Other readers come from the world of PCs, where tools such as dBASE,
Paradox, Access, or FoxPro are considered to be the standard database tools. Overemphasizing
the huge gap that exists between these two worlds is almost impossible.
Readers who are familiar with "big iron" and large network-based servers
are likely to find the Local InterBase Server very familiar. Readers who come from
the world of PCs are likely to find InterBase very strange indeed, especially at
first.
InterBase is meant to handle huge numbers of records, which are stored on servers.
It does not come equipped with many of the amenities of a tool such as dBASE or Paradox.
In fact, InterBase supplies users with only the most minimal interface and instead
expects you to create programs with a client-side tool such as BCB. However, you
will find that InterBase is not a particularly difficult challenge, after you get
some of the basics under your belt.
Databases and the Job Market
Having been in this business for a while, I know that most of the readers of this
book probably work inside a corporation or at a small company. However, if you are
a student or someone who wants to enter the computer programming world, you should
pay special attention to the material in this, and other, chapters on InterBase.
Perhaps 80 percent of the applications built in America today use databases in
one form or another. Indeed, most of these applications revolve around, and are focused
on, manipulating databases. Furthermore, client/server databases such as InterBase,
Oracle, or MS SQL Server form the core of this application development.
If you want to enter the programming world, getting a good knowledge of databases
is one of the best ways to get started. Right now, there is virtually an endless
need for good database programmers.
One note of caution should perhaps be added here. I happen to enjoy database programming.
However, it is not the most romantic end of the computer business. If you're primarily
interested in systems programming or games programming, then you should hold out
for jobs in those fields rather than focus your career in an area of only minor interest
to you.
Databases, however, offer the greatest opportunity for employment. In particular,
client/server database programmers are almost always in demand. Because LIBS ships
with your copy of BCB, you have a great chance to learn the ins and outs of this
lucrative field.
Setting Up the Local InterBase
LIBS is installed for you automatically when you install BCB. In most cases, InterBase
will run smoothly without any need for you to worry about setup. However, you should
take several key steps to ensure that all is as it should be.
First, find out if LIBS is running. By default, it will load into memory every
time you boot up the system. If you're running Windows 95 or NT 4.0, you should see
LIBS as a little splash of green on the system tray to the right of the toolbar.
On Windows NT 3.51, an icon appears at the bottom of your screen. Whatever shape
it takes on your system, just click this green object, and you will see a report
on the Local InterBase Server configuration.
You need to know where your copy of LIBS is installed. Most likely, it is in the
..\PROGRAM FILES\BORLAND\INTRBASE subdirectory on the boot drive of your
computer. For example, my copy of the local InterBase is in C:\PROGRAM FILES\BORLAND\INTRBASE.
To find out for sure, open the InterBase Configuration applet that ships with BCB.
It will report on the InterBase root subdirectory and enable you to change that directory
if need be.
To find this same information in the Registry, run REGEDIT.EXE and open
HKEY_LOCAL_MACHINE/SOFTWARE/BORLAND/INTERBASE. Several nodes report on the
location of your server and other related information.
In the INTRBASE subdirectory, you will find a copy of a file called INTERBAS.MSG.
You should also be able to locate a copy of GDS32.DLL somewhere on your
system, most likely in the ..\WINDOWS\SYSTEM subdirectory, but possibly
in either your BDE or INTRBASE subdirectory.
A common problem occurs when InterBase users end up with more than one copy of
GDS32.DLL. If you work with the networked version of InterBase, you probably
already have a copy of the InterBase Client on your system. If this is the case,
you should make sure that you don't have two sets of the file GDS32.DLL
on your path. On my system, I use the copy of GDS32.DLL that comes with
the local InterBase. These tools communicate with both LIBS and the full networked
version of InterBase. This setup works fine for me. However, the point is not which
version you use, but only that you know which version is on your path and that you
have only one version on your system at a time.
To find out which version you are currently using, run the InterBase Communications
Diagnostics Tool that ships with BCB. Use the Browse button to find the EMPLOYEE.GDB
file, which is probably located in the ..PROGRAM FILES\BORLAND\INTRBASE\EXAMPLES
subdirectory. Enter SYSDBA as the user name and masterkey as the
password, all lowercase. (This example assumes that you have not changed the password
from its default value.) You should get the following readout, or something like
it:
Path Name = C:\WINDOWS\SYSTEM\gds32.dll
Size = 348672 Bytes
File Time = 04:10:00
File Date = 12/18/1995
Version = 4.1.0.6
This module has passed the version check.
Attempting to attach to C:\Borland\Intrbase\EXAMPLES\Employee.gdb
Attaching ...Passed!
Detaching ...Passed!
InterBase versions for this connection:
InterBase/Windows NT (access method), version "WI-B4.1.0"
on disk structure version 8.0
InterBase Communication Test Passed!
The key piece of information you're getting here is the location of GDS32.DLL.
NOTE: Readers who want to connect to the
full server version of InterBase will find that the procedure I have just outlined
works fine, except that you must have a network protocol such as TCP/IP loaded first.
This book includes a description of the wizardry needed to set up a TCP/IP network
protocol successfully in Chapter 8, "Database Basics and Database Tools."
As I explained in that chapter, this task is usually handled automatically by either
Windows 95 or Windows NT, though it helps to have a few extra tips to guide you through
the process. I should add that setting up an InterBase connection is usually a fairly
straightforward process when compared to setting up other servers.
The most obvious thing that can go wrong with an InterBase connection is simply
that it is not being started automatically when you start Windows. If you are having
trouble, try simply pointing the Explorer to the IntrBase/bin subdirectory
and clicking the IBServer.exe icon. The trouble could be that all is set
up correctly, but for some reason the server is not currently running on your machine!
Setting Up an InterBase Alias
In the preceding section, you learned how to run a diagnostic tool to be sure
you are connected to InterBase. This section deals with the issue of making sure
that the BDE is connected to InterBase. In other words, that section dealt with making
sure that InterBase was running correctly on your machine; this section deals with
making sure BCB is connected to InterBase. You should also check the readme file
on the CD that accompanies this book for general information about setting up aliases
for the programs supplied in this book.
After you have the local InterBase set up, you should take a few minutes to make
sure the connection to the BDE is working correctly. In particular, you should make
sure an alias points to one of the sample tables that ship with LIBS. For example,
after a normal full installation of BCB, you should have an alias called IBLOCAL
that points to the EMPLOYEE.GDB file.
In the next few paragraphs, I describe how to set up an alias identical to the
IBLOCAL alias, except you can give it a different name. To begin, open the
Database Explorer and turn to the Databases page. Select the first node in the tree,
the one that's called Databases. Choose Database | New, and then select
IntrBase as the Database Driver Name in the New Database Alias page dialog. Click
OK.
Name the new alias TESTGDB, or give it whatever name you prefer. The
ServerName property for this alias should be set to
c:\program files\borland\intrbase\examples\employee.gdb
You can adjust the drive letter and path to reflect the way you have set up the
files on your machine.
The user name should be set to SYSDBA, and the default password you will
use is masterkey. (If someone has changed the password on your system, then
use the new password.) All the other settings in the Database Explorer can have their
default values, as shown in Figure 15.1. After you have everything set up correctly,
choose Database | Apply.
FIGURE
15.1. A sample InterBase alias as it appears
in the Database Explorer.
After you have set up and saved your alias, you can connect to the TESTGDB
alias exactly as you would with any other set of data. From inside the Explorer,
just click the plus symbol before the TESTGDB node. A dialog will pop up
prompting you for a password. Make sure the user name is set to SYSDBA,
and then enter masterkey as the password. Everything else will then be the
same as when working with a Paradox table, except that you will find many new features
such as stored procedures and triggers. Most of these new features will be described
in this chapter and the next.
To connect to the database from inside BCB proper, first drop a table onto a form,
and set its DatabaseName property to TESTGDB. When you try to drop
down the list of TableNames, you will be prompted for a password. You should
enter masterkey at this point, all in lowercase. Now drop down the list
again and select a table. After taking these steps, you can set the Active
property for Table1 to True. If this call succeeds, everything
is set up correctly, and you can begin using the local InterBase to create BCB database
programs. If you can't set Active to True, you should go over the
steps outlined previously and see whether you can correct the problem.
NOTE: I usually use SYSDBA and
masterkey as the user name and password combination for the InterBase databases
in this book. However, I sometimes work with USER1 and USER1 instead,
simply because typing USER1 is easier than typing masterkey. One
way to change the sign on criteria for InterBase is via the InterBase Server Manager.
This, and other tools, will be discussed in Chapter 16, "Advanced InterBase
Concepts."
In the preceding two sections, you have learned the basic facts about using LIBS.
The next step is to learn how to create your own databases and tables.
Creating Databases
Unlike local Paradox or dBASE files, InterBase tables are not stored in separate
files located within a directory. Instead, InterBase tables are stored in one large
file called a database. Therefore, you need to first go out and create a database,
and then you can create a series of tables inside this larger database.
NOTE: The single file system is, in my
opinion, vastly superior to having a series of separate files. I'm sure you've noticed
what happens after you have placed a few indexes on a typical Paradox table. The
end result is that your table is associated with six or seven other files, some of
which have to be present or you can't get at your data! A big Paradox database might
consist of a hundred or more files, all of which have to be backed up, moved from
place to place, and maintained. Life is much simpler when your whole database is
stored in a single file!
The simplest way to create a database is with a CASE tool such as SDesigner or
Cadet. However, these tools do not ship with BCB, so you must instead choose between
the Database Desktop, BCB itself, and the WISQL program that ships with the Local
InterBase Server. Absent the presence of a CASE tool, I find that my weapon of choice
is WISQL, though this is certainly a debatable decision. (Cadet will be discussed
briefly, along with SDesigner, in Chapter 18, "Working with CASE Tools: Cadet,
ER1, and SDesigner.")
WISQL stands for Windows Interactive Standard Query Language, or simply the Interactive
SQL tool. WISQL is fundamentally a tool for entering SQL statements, with a few other
simple features thrown in for good measure. One advantage of relying on WISQL is
that it allows you to work directly in the mother tongue of databases, which is SQL.
I find that defining databases directly in SQL helps me understand their structure,
though of course, there is little reason for resorting to these measures if you have
a copy of SDesigner or ERWin available.
You should also remember that WISQL bypasses the BDE altogether. You can therefore
use it to test your connections to InterBase even if you are not sure that you have
the BDE set up correctly. For example, if you're having trouble connecting to InterBase
and you're not sure where the problem lies, start by trying to connect with WISQL.
If that works but you can't connect from inside BCB, the problem might lie not with
your InterBase setup, but with the way you have deployed the BDE.
NOTE: In addition to WISQL, the other
important tool that ships with InterBase is the InterBase Server Manager, IBMGR.EXE.
It enables you to test connections to servers and perform simple maintenance tasks
such as backing up and restoring databases and setting passwords. You can use IBMGR
to back up your data so that you can recover if disaster strikes. What little information
you need for this easy-to-use tool is available in the InterBase documentation and
in the short section on this tool that appears later in this chapter.
After starting WISQL, choose File | Create Database. A dialog like the one shown
in Figure 15.2 appears. Set the Location Info to Local Engine, because you are in
fact working with local InterBase. (Actually, there is no reason that you have to
use Local InterBase rather than the full server version when working through these
examples. However, I will reference LIBS throughout this chapter because it will
be the tool of choice for most readers.)
FIGURE
15.2. The dialog used for creating databases
inside WISQL.
In the Database field, enter the name of the table you want to create. If the
table is to be located inside a particular directory, include that directory in the
database name. For practice, you should create a database called INFO.GDB
that is located in a subdirectory called DATA. If it does not already exist
on your system, you should first go to DOS or the Windows Explorer and create the
DATA subdirectory. After you set up the subdirectory, enter the following
in the Database field:
E:\DATA\INFO.GDB
You can replace E: with the letter for the appropriate drive on your
system. The extension .GDB is traditional, though not mandatory. However,
I suggest always using this extension so that you can recognize your databases instantly
when you see them. Accidentally deleting even a recently backed up database can be
a tragedy.
You can set the user name to anything you want, although the traditional entry
is SYSDBA, and the traditional password is masterkey. When you
first start out with InterBase, sticking with this user name and password combination
is probably best. Even if you assign new passwords to your database, the SYSDBA/masterkey
combination will still work unless you explicitly remove it using the IBMGR.
After you have entered a user name and password, you can create the database by
clicking the OK button. If all goes well, you are then placed back inside WISQL proper.
At this stage, you can either quit WISQL or add a table to your database. If something
goes wrong, an error message will appear. Click the Details button to try to track
down the problem.
Assuming all goes well, the following SQL statement can be run inside WISQL if
you want to create a very simple table with two fields:
CREATE TABLE TEST1 (FIRST VARCHAR(20), LAST INTEGER);
Enter this line in the SQL Statement field at the top of WISQL, and then
click the Run button. If all goes smoothly, your statement will be echoed in the
ISQL output window without being accompanied by an error dialog. The lack of an error
dialog signals that the table has been created successfully.
The preceding CREATE TABLE command creates a table with two fields. The
first is a character field containing 20 characters, and the second is an integer
field.
NOTE: The table-creation code shown here
is used to describe or create a table in terms that WISQL understands. In fact, you
can use this same code inside a TQuery object in a BCB program.
Throughout most of this chapter and the next, I work with WISQL rather than with
the DBD. In describing how to perform these actions in WISQL, I do not mean to imply
that you can't use the Database Desktop to create or alter InterBase tables. In fact,
the 32-bit version of DBD provides pretty good support for InterBase tables. Still,
I have found WISQL to be considerably more powerful than I suspected when I first
started using it. Once again, I should add that neither of these tools is as easy
to use as a good CASE tool.
After creating a database and table, you should choose File | Commit Work. This
command causes WISQL to actually carry out the commands you have issued. At this
stage, you should choose File | Disconnect from Database.
In this section, you have learned the basic steps required to use InterBase to
create a database and table. The steps involved are not particularly complicated,
although they can take a bit of getting used to if you're new to the world of SQL.
Exploring a Database with WISQL
WISQL provides a number of tools that can help you explore a database and its
contents. In the preceding section, you created a database with a single table. In
this section, you will learn how to connect to the database and table from inside
WISQL. You will also see how to examine the main features of the entities you have
created.
To connect to INFO.GDB, choose File | Connect to Database, which brings
up the dialog shown in Figure 15.3. Enter the drive and the database as e:\data\info.gdb,
where e: represents the appropriate drive on your machine. Enter the user
as SYSDBA, and the password as masterkey. If all goes well, you
should be able to connect to the database by clicking the OK button. Once again,
success is signaled by the lack of an error message.
FIGURE
15.3. Connecting to the INFO.GDB
database using WISQL.
Choose View | Metadata Information and set View Information On to Database, as
shown in Figure 15.4. After you click OK, the information displayed in the ISQL output
window should look something like this:
SHOW DB
Database: c:\data\info.gdb
Owner: SYSDBA
PAGE_SIZE 1024
Number of DB pages allocated = 210
Sweep interval = 20000
FIGURE
15.4. Preparing to view information on
the INFO.GDB database.
To see the tables available in a database, choose View | Metadata Information,
and set View Information On to Table. You can leave the edit control labeled Object
Name blank. If you fill it in with a table name, you will get detailed information
on a specific table--but in this case we want general information on all tables.
Click the OK button and view the information, which should look like the following,
in the ISQL output window:
SHOW TABLES
TEST1
Browsing through the Metadata Information menu choice, you can see that InterBase
supports triggers, stored procedures, views, and a host of other advanced server
features.
The Extract menu choice enables you to find out more detailed information about
the database and its tables. For example, if you choose Extract | SQL Metadata for
a Database, you get output similar to the following:
/* Extract Database e:\data\info.gdb */
CREATE DATABASE "e:\data\info.gdb" PAGE_SIZE 1024
;
/* Table: TEST1, Owner: SYSDBA */
CREATE TABLE TEST1 (FIRST VARCHAR(20),
LAST INTEGER);
/* Grant permissions for this database */
If you choose Extract | SQL Metadata for Table, you get the following output:
/* Extract Table TEST1 */
/* Table: TEST1, Owner: SYSDBA */
CREATE TABLE TEST1 (FIRST VARCHAR(20),
LAST INTEGER);
You should note that WISQL often asks whether you want to save the output from
a command to a text file, and the File menu gives you some further options for saving
information to files. You can take advantage of these options when necessary, but
90 percent of the time, I pass them by with barely a nod. (Some CASE tools use the
output from Extract | SQL Metadata to reverse-engineer a database. If your CASE tool
asks you for a script file, you can produce one this way.)
NOTE: The WISQL program accepts most SQL
statements. For example, you can perform Insert, Select, Update,
and Delete statements from inside WISQL. Just enter the statement you want
to perform in the SQL Statement area, and then click the Run button.
WISQL also comes equipped with a handy online reference to SQL. If you have questions
about how to format an Alter, Drop, Insert, Create
Index, or other SQL statement, you can look it up in the help for WISQL.
(For better or worse, this is my number-one reference for SQL statements. Another
book I have found useful is called The Practical SQL Handbook, by Bowman, Emerson,
and Darnovsky, Addison Wesley, ISBN 0-201-62623-3.)
After reading the preceding three sections, you should have a fair understanding
of how WISQL works and how you can use it to manage a database. The information provided
in this chapter is nothing more than an introduction to a complex and very sophisticated
topic. However, you now know enough to begin using the local InterBase. This accomplishment
is not insignificant. Tools such as InterBase, Oracle, and Sybase lie at the heart
of the client/server activity that is currently so volatile and lucrative. If you
become proficient at talking to servers such as InterBase, you might find yourself
at an important turning point in your career.
Transactions
Now you can break out of the abstract theory rut and start writing some code that
actually does something. In this section, you will look at transactions, followed
by a discussion of cached updates and many-to-many relationships. In the next chapter,
you will see another "real-world" database, when you take a look at a sample
program that tracks the albums, tapes, and CDs in a music collection.
The TRANSACT program, found on the CD that accompanies this book, gives
a brief introduction to transactions. To use transactions, you must have a TDataBase
component on your form. Transactions work not only with real servers such as Sybase,
Informix, InterBase, or the local InterBase, but also with the 32-bit BDE drivers
for Paradox or dBASE files. In other words, transactions can be part of most of the
database work you will do with BCB. Using transactions is, however, a technique most
frequently associated with client/server databases.
To begin, drop down a TDatabase component on a TDataModule.
Set the AliasName property of the TDataBase object to a valid alias
such as IBLOCAL. Create your own string, such as TransactionDemo,
to fill in the DatabaseName property of the TDatabase object. In
other words, when you're using a TDatabase component, you make up the DatabaseName
rather than pick it from a list of available aliases.
Drop down a TQuery object, and hook it up to the EMPLOYEE.GDB
file that ships with BCB. In particular, set the DatabaseName property of
the TQuery object to TransactionDemo, not to IBLOCAL.
In other words, set the DatabaseName property to the string you made up
when filling in the DatabaseName property of the TDatabase component.
You will find that TransactionDemo, or whatever string you chose, has been
added to the list of aliases you can view from the Query1.DatabaseName Property
Editor. Now rename Query1 to EmployeeQuery and attach a TDataSource
object called EmployeeSource to it.
Finally, set the EmployeeQuery->SQL property to the following string:
select * from employee
Then set the Active property to True and set RequestLive
to True.
Add a TTable object to the project, hook it up to the SALARY_HISTORY
table, and call it SalaryHistoryTable. Relate the SalaryHistoryTable
to the EmployeeQueryTable via the EMP_NO fields of both tables.
In particular, you should set the MasterSource property for the SalaryHistoryTable
to EmployeeSource. Then click the MasterFields property of the
TTable object, and relate the EMP_NO fields of both tables. This
way, you can establish a one-to-many relationship between the EmployeeQueryTable
and the SalaryHistoryTable.
After you're connected to the database, you can add two grids to your main form
so that you can view the data. Remember that you should use the File | Include Unit
Header option to link the TDataModule to the main form.
On the surface of the main form, add four buttons, and give them the following
captions:
Start Transaction
Rollback
Commit
Refresh
The code associated with these buttons should look like this:
void __fastcall TForm1::StartTransactionBtnClick(TObject *Sender)
{
DMod->TransDemo->StartTransaction();
}
void __fastcall TForm1::RollbackBtnClick(TObject *Sender)
{
DMod->TransDemo->Rollback();
}
void __fastcall TForm1::CommitBtnClick(TObject *Sender)
{
DMod->TransDemo->Commit();
}
void __fastcall TForm1::RefreshBtnClick(TObject *Sender)
{
DMod->SalaryHistoryTable->Refresh();
}
Run the program, click Start Transaction and edit a record of the SalaryHistoryTable.
When you do so, be sure to fill in all the fields of the table except for the first
and last, which are called EMP_NO and NEW_SALARY. Be sure not to
touch either of those fields, as they will be filled in for you automatically. In
particular, you might enter the following values:
CHANGE_DATE: 12/12/12
UPDATER_ID: admin2
OLD_SALARY: 105900
PERCENT_CHANGE: 3
These values are not randomly chosen. For example, you have to enter admin2,
or some other valid UPDATE_ID, in the UPDATER_ID field. You can,
of course, enter whatever values you want for the date, old salary, and percent change
fields. Still, you need to be careful when working with the Employee tables. This
database has referential integrity with a vengeance!
After entering the preceding values, you can post the record by moving off it.
When you do, the NEW_SALARY field will be filled in automatically by something
called a trigger. Go ahead and experiment with these tables some if you want. For
example, you might leave some of the fields blank, or enter invalid data in the UPDATER_ID
field, just to see how complex the rules that govern this database are. This data
is locked up tighter than Fort Knox, and you can't change it unless you are very
careful about what you're doing. (It's worth noting, however, that the developers
of this database probably never planned to have anyone use these two tables exactly
as I do here. Defining rules that limit how you work with a database is easy, but
finding ways to break them is easier still. For all of its rigor, database programming
is still not an exact science.)
If you started your session by clicking the Start Transaction button, you can
now click RollBack and then Refresh. You will find that all your work is undone,
as if none of the editing occurred. If you edit three or four records and then click
Commit, you will find that your work is preserved.
NOTE: Though you are safe in this particular
case, in some instances like this you can't call Refresh directly because
the table you're using is not uniquely indexed. In lieu of this call, you can close
the table and then reopen it. You could use bookmarks to preserve your location in
the table during this operation, or if you're working with a relatively small dataset,
as in this example, you can just let the user fend for himself or herself.
Note that when you run the TRANSACT program included on the CD, you don't have
to specify a password because the LoginPrompt property of the TDatabase
object is set to False, and the Params property contains the following
string:
password=masterkey
Now that you have seen transactions in action, you probably want a brief explanation
of what they are all about. Here are some reasons for using transactions:
- 1. To ensure the integrity of your data. Sometimes you need to perform
a transaction that effects several different interrelated tables. In these cases,
it might not be a good idea to alter two tables and then find the session is interrupted
for some reason before you can alter the next two tables. For example, you might
find that a data entry clerk posts data to two records, but the system crashes before
he can finish updating two more records in a different table. As a result, the data
in your database may be out of sync. To avoid this situation, you can start a transaction,
edit all the rows and tables that need to be edited, and then commit the work in
one swift movement. This way, an error is far less likely to occur because of a system
crash or power failure.
2. To handle concurrency issues in which two or more people are accessing
the same data at the same time. You can use a transactions feature called TransIsolation
levels to fine-tune exactly how and when updates are made. This way, you can decide
how you will react if another user is updating records exactly on or near the record
you're currently editing.
Now that you have read something about the theory behind transactions, you might
want to think for a moment about the TransIsolation property of the TDatabase
object, which affects the way transactions are handled. Here are some quotes from
the very important online help entry called "Transaction Isolation Levels."
| tiDirtyRead |
Permits reading of uncommitted changes made to the database by other simultaneous
transactions. Uncommitted changes are not permanent, and might be rolled back (undone)
at any time. At this level a transaction is least isolated from the effects of other
transactions. |
| tiReadCommitted |
Permits reading of committed (permanent) changes made to the database by other simultaneous
transactions. This is the default TransIsolation property value. |
| tiRepeatableRead |
Permits a single, one-time reading of the database. The transaction cannot see any
subsequent changes made by other simultaneous transactions. This isolation level
guarantees that after a transaction reads a record, its view of that record does
not change unless it makes a modification to the record itself. At this level, a
transaction is most isolated from other transactions. |
Most of the time, you can simply leave this field set to tiReadCommitted.
However, it is important to understand that you have several options regarding how
the data in your database is affected by a transaction. The whole subject of how
one user of a database might alter records in a table while they are being used by
another user is quite complicated, and it poses several paradoxes for which no simple
solution exists. The preceding TransIsolation levels allow you to choose
your poison when dealing with this nasty subject.
You must consider other issues when you're working with transactions, but I have
tried to cover some of the most important here. In general, I find that transactions
are extremely easy to use. However, they become more complex when you consider the
delicate subject of concurrency problems, which are frequently addressed through
setting the TransIsolation levels of your transactions.
Cached Updates
Cached updates are like the transactions just described, except that they enable
you to edit a series of records without causing any network traffic. When you are
ready to commit your work, cached updates enable you to do so on a record-by-record
basis, where any records that violate system integrity can be repaired or rolled
back on a case-by-case basis.
NOTE: Some users have reported remarkable
increases in performance on some operations when they use cached updates.
The key feature of cached updates is that they let you work with data without
allowing any network traffic to occur until you are ready for it to begin. This relatively
complex mechanism also enables you to keep track of the status of each record on
a field-by-field basis. In particular, when cached updates are turned on, you can
query your records one at a time and ask them whether they have been updated. Furthermore,
if they have been updated, you can ask the current value of each field in the updated
record, and you can also retrieve the old, or original, value of the field.
You can do three things with the records in a dataset after the CachedUpdates
property for the dataset has been set to True:
- 1. You can call ApplyUpdates on the dataset, which means that
you will try to commit all the other records updated since CachedUpdates
was set to True or since the last attempt to update the records. This is
analogous to committing a transaction.
2. You can call CancelUpdates, which means that all the updates made
so far will be canceled. This is analogous to rolling back a transaction.
3. You can call RevertRecord, which will roll back the current record,
but not any of the other records in the dataset.
An excellent sample program in the BCB DEMOS subdirectory shows how to
use cached updates. This program is a bit complex in its particulars, however, and
therefore can be hard to understand. So, instead of trying to go it one better, I
will create a sample program that takes the basic elements of cached updates and
presents them in the simplest possible terms.
The CacheUp program, shown in Figure 15.5, has one form. On the form
is a copy of the OrdersTable. The OrdersTable, as you recall, is
related to both the Customer table and the Items table. As a result,
changing either the OrderNo or CustNo fields without violating
system integrity in one way or another is difficult. When working with this program,
you should change these fields to values like 1 or 2, which will
almost surely be invalid. You can then watch what happens when you try to commit
the records you have changed.
The code for the CachedUpdates program is shown in Listing 15.1. Go ahead and
get this program up and running, and then come back for a discussion of how it works.
When you're implementing the code shown here, the key point to remember is that none
of it will work unless the CachedUpdates property of the OrdersTable
is set to True.
Listing 15.1. The form for the CachedUpdates
program.
///////////////////////////////////////
// File: Main.cpp
// Project: CachedUpdates
// Copyright (c) 1997 Charlie Calvert
#include <vcl\vcl.h>
#include <typinfo.hpp>
#include <sysutils.hpp>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::ApplyBtnClick(TObject *Sender)
{
OrdersTable->ApplyUpdates();
}
void __fastcall TForm1::RevertBtnClick(TObject *Sender)
{
OrdersTable->RevertRecord();
}
void __fastcall TForm1::CancelClick(TObject *Sender)
{
OrdersTable->CancelUpdates();
}
void __fastcall TForm1::OkBtnClick(TObject *Sender)
{
Close();
}
void __fastcall TForm1::OrdersTableUpdateError(TDataSet *DataSet,
EDatabaseError *E, TUpdateKind UpdateKind, TUpdateAction &UpdateAction)
{
TTypeInfo TypeInfo;
AnsiString UpdateKindStr[] = {"Modified", "Inserted", "Deleted"};
AnsiString S(UpdateKindStr[UpdateKind]);
S += ": " + E->Message;
AnsiString Temp = DataSet->Fields[0]->OldValue;
Temp = + ": " + S;
ListBox1->Items->Add(Temp);
UpdateAction = uaSkip;
}
void __fastcall TForm1::DataSource1DataChange(TObject *Sender,
TField *Field)
{
AnsiString UpdateStat[] = {"Unmodified", "Modified", "Inserted", "usDeleted"};
Panel1->Caption = UpdateStat[OrdersTable->UpdateStatus()];
if (OrdersTable->UpdateStatus() == usModified)
{
Edit1->Text = OrdersTable->Fields[0]->OldValue;
Edit2->Text = OrdersTable->Fields[0]->NewValue;
}
else
{
Edit1->Text = "Unmodified";
Edit2->Text = "Unmodified";
};
}
void __fastcall TForm1::ListBox1DblClick(TObject *Sender)
{
AnsiString S(ListBox1->Items->Strings[ListBox1->ItemIndex]);
if (S.Length() > 0)
ShowMessage(S);
}
The first thing to notice about the CachedUpdates program is that it tracks which
records have been modified. For example, change the OrderNo field of the
first two records to the values 1 and 2. If you now select one
of these records, you will see that the small panel in the lower left corner of the
screen gets set to Modified. This means that the update status for this
field has been set to modified.
Here is the TUpdateStatus type:
TUpdateStatus = (usUnmodified, usModified, usInserted, usDeleted);
Any particular record in a database is going to be set to one of these values.
Here is the code that sets the value in the TPanel object:
void __fastcall TForm1::DataSource1DataChange(TObject *Sender,
TField *Field)
{
AnsiString UpdateStat[] = {"Unmodified", "Modified", "Inserted", "usDeleted"};
Panel1->Caption = UpdateStat[OrdersTable->UpdateStatus()];
if (OrdersTable->UpdateStatus() == usModified)
{
Edit1->Text = OrdersTable->Fields[0]->OldValue;
Edit2->Text = OrdersTable->Fields[0]->NewValue;
}
else
{
Edit1->Text = "Unmodified";
Edit2->Text = "Unmodified";
};
}
The relevant line in this case is the second in the body of the function. In particular,
notice that it reports on the value of OrdersTable->UpdateStatus. This
value will change to reflect the update status of the currently selected record.
NOTE: Notice that in this case, I explicitly
type out the names associated with the elements of the TUpdateStatus type.
In some cases, you can use an alternative means to accomplish the same end without
explicitly typing out the strings. This second technique involves using the advanced
RTTI supported by BCB. To show this value to the user, the code could call the GetEnumName
routine from the TypInfo unit. This routine retrieves the name of an enumerated
value. To use this routine, pass in the type that you want to examine, as well as
the ordinal value of the element in that type whose name you want to see:
PPropInfo PropInfo =
GetPropInfo(PTypeInfo(ClassInfo(__classid(TForm1))), "Borderstyle");
ShowMessage(GetEnumName(PropInfo->PropType, int(bsDisabled)));
Unfortunately, this type of code will work only for VCL-style classes and for
properties of VCL-style classes. Because a TDataSet does not have an UpdateStatus
property, the code in the DataSource1DataChange method must use the more
pedantic method outlined previously.
At the same time that the CachedUpdates program reports that a record has been
modified, it also reports on the old and new value of the OrderNo field
for that record. In particular, if you change the first record's OrderNo
field to 1, it will report that the old value for the field was 1003,
and the new value is 1. (This assumes that you have the original data as
it shipped with BCB. Remember that if you end up ruining one of these tables performing
these kinds of experiments, you can always copy the table over again from the CD.)
In the code that reports on the old and new value of the OrderNo field,
you should examine these lines in particular:
Edit1->Text = OrdersTable->Fields[0]->OldValue;
Edit2->Text = OrdersTable->Fields[0]->NewValue;
As you can see, this information is easy enough to come by--you just have to know
where to look.
If you enter the values 1 and 2 into the OrderNo fields
for the first two records, you will encounter errors when you try to commit the data.
In particular, if you try to apply the data, the built-in referential integrity will
complain that there is no way to link the Orders and Items table
on the new OrderNo you have created. As a result, committing the records
is not possible. The code then rolls back the erroneous records to their original
state.
When viewing these kinds of errors, choose Options | Environment | Preferences
and then turn off the Break on Exception option. The issue here is that you want
the exception to occur, but you don't want to be taken to the line in your program
where the exception surfaced. You don't need to view the actual source code because
these exceptions are not the result of errors in your code. In fact, these exceptions
are of the kind you want and need to produce and which appear to the user in an orderly
fashion via the program's list box.
NOTE: Referential integrity is a means
of enforcing the rules in a database. This subject is discussed in some detail in
Chapter 16 and also in Chapter 12, "Understanding Relational Databases."
For now, you should not be concerned with the details of how referential integrity
works. The key point is simply that some tables have to obey rules, and the BDE will
not let users enter invalid data that violates these rules.
Here is the code that reports on the errors in the OrderNo field and
rolls back the data to its original state:
void __fastcall TForm1::OrdersTableUpdateError(TDataSet *DataSet,
EDatabaseError *E, TUpdateKind UpdateKind, TUpdateAction &UpdateAction)
{
TTypeInfo TypeInfo;
AnsiString UpdateKindStr[] = {"Modified", "Inserted", "Deleted"};
AnsiString S(UpdateKindStr[UpdateKind]);
S += ": " + E->Message;
AnsiString Temp = DataSet->Fields[0]->OldValue;
Temp = + ": " + S;
ListBox1->Items->Add(Temp);
UpdateAction = uaSkip;
}
This particular routine is an event handler for the OnUpdateError event
for the Table1 object. To create the routine, click once on the Table1
object, select its Events page in the Object Inspector, and then double-click the
OnUpdateError entry.
The OrdersTableUpdateError method will get called only if an error occurs
in attempting to update records. It will get called at the time the error is detected
and before BCB tries to commit the next record.
OrdersTableUpdateError gets passed four parameters. The most important
is the last, which is a var parameter. You can set this parameter to one
of the following values:
TUpdateAction = (uaAbort, uaSkip, uaRetry, uaApplied);
If you set the UpdateAction variable to uaAbort, the entire
attempt to commit the updated data will be aborted. None of your changes will take
place, and you will return to edit mode as if you had never attempted to commit the
data. The changes you have made so far will not be undone, but neither will they
be committed. You are aborting the attempt to commit the data, but you are not rolling
it back to its previous state.
If you choose uaSkip, the data for the whole table will still be committed,
but the record that is currently in error will be left alone. That is, it will be
left at the invalid value assigned to it by the user.
If you set UpdateAction to uaRetry, that means you have attempted
to update the information in the current record and that you want to retry committing
it. The record you should update is the current record in the dataset passed as the
first parameter to OrdersTableUpdateError.
In the OrdersTableUpdateError method, I always choose uaSkip
as the value to assign to UpdateAction. Of course, you could pop up a dialog
and show the user the old value and the new value of the current record. The user
would then have a chance to retry committing the data. Once again, you retrieve the
data containing the current "problem child" record from the dataset passed
in the first parameter of OrdersTableUpdateError. I show an example of accessing
this data when I retrieve the old value of the OrderNo field for the record:
AnsiString Temp = DataSet->Fields[0]->OldValue;
Temp = + ": " + S;
ListBox1->Items->Add(Temp);
Needless to say, the OldValue field is declared as a Variant
in the source code to DB.HPP, which is the place where the TDataSet
declaration is located:
System::Variant __fastcall GetOldValue(void);
...
__property System::Variant OldValue = {read=GetOldValue};
Two other values are passed to the TableUpdateError method. The first
is an exception reporting on the current error, and the second is a variable of type
TUpdateKind:
enum TUpdateKind { ukModify, ukInsert, ukDelete };
The variable of type TUpdateKind just tells you how the current record
was changed. Was it updated, inserted, or deleted? The exception information is passed
to you primarily so that you can get at the message associated with the current error:
E->Message;
If you handle the function by setting UpdateAction to a particular value,
say uaSkip, then BCB will not pop up a dialog reporting the error to the
user. Instead, it assumes that you are handling the error explicitly, and it leaves
it up to you to report the error or not, as you see fit. In this case, I just dump
the error into the program's list box, along with some other information.
That's all I'm going to say about cached updates. At this point, you should go
back and run the Cache program that ships with BCB. It covers all the same ground
covered in the preceding few pages, but it does so in a slightly different form.
In particular, it shows how to pop up a dialog so that you can handle each OnUpdateError
event in an intelligent and sensible manner.
In general, cached updates give you a great deal of power you can tap into when
updating the data in a dataset. If necessary, go back and play with the CachedUpdates
program until it starts to make sense to you. This subject isn't prohibitively difficult,
but it does take a few moments' thought to absorb the basic principles involved.
Many-to-Many Relationships
Many-to-many relationships are necessities in most relational database projects.
Suppose, for example, that you have a set of software routines that you want to store
in a database. Some of the routines you can use in DOS, some in UNIX, some in Windows
NT, and some in Windows 95. Some routines, however, apply to two or more of the operating
systems.
To track this information, you might try adding an OS field to your Routines table,
where OS stands for Operating System. This solution sounds simple enough. However,
there is one problem. The issue, of course, is that some routines will work with
more than one OS. For example, you may have a routine that works in Windows NT and
Windows 95, but not in UNIX or DOS. As a result, the fairly simple one-to-many relationship
you try to establish with the OS fields really needs to be converted into a many-to-many
relationship.
Here, for example, is a list of operating systems:
| CODE |
OS |
| 1 |
DOS |
| 2 |
UNIX |
| 3 |
Windows |
Here is a list of routines:
| CODE |
FUNCTION_NAME |
OSCODE |
| 1 |
FormatDriveC |
1 |
| 2 |
AssignAllIRQsToTheMouse |
2 |
As you can see, the format shown here allows you to assign only one OSCODE
to each routine. The goal is to find a way to specify that a routine works in more
than one OS. As you will see, one good solution involves creating a third table that
stands in the middle, between the OS and FUNCTION tables shown here.
The rest of this section describes how to actually go about creating many-to-many
relationships. This subject is annoyingly complex, but one that you can master if
you take a little time to think things through. My basic goal is to break down this
process into a series of steps that you can follow whenever you have to create one
of these many-to-many relationships. I might be going too far to say that these steps
make the process simple. They do make it manageable, however.
In Listing 15.2, you will find the database definition for a simple set of InterBase
tables. You can run this definition through WISQL by choosing File | Run ISQL Script.
Alternatively, you can create a new database and pass through the key statements
shown here one at a time.
Beneath the data definition for the database, you will find the code to a program
called ManyToMany. This code, in Listings 15.3 through 15.5, shows how to handle
a many-to-many relationship in a BCB program.
Listing 15.2. The schema for a simple
database that can capture a many-to-many relationship.
/* Extract Database c:\src\unleash2\data\man2man.gdb */
CREATE DATABASE "c:\src\unleash2\data\man2man.gdb" PAGE_SIZE 1024;
/* Table: ATTRIBS, Owner: SYSDBA */
CREATE TABLE ATTRIBS (ATTRIBNO INTEGER NOT NULL,
ATTRIB VARCHAR(34),
PRIMARY KEY (ATTRIBNO));
/* Table: CUSTOMERS, Owner: SYSDBA */
CREATE TABLE CUSTOMERS (CUSTNO INTEGER NOT NULL,
NAME VARCHAR(35),
PRIMARY KEY (CUSTNO));
/* Table: MIDDLE, Owner: SYSDBA */
CREATE TABLE MIDDLE (CUSTNO INTEGER,
ATTRIBNO INTEGER);
/* Grant permissions for this database */
Listing 15.3. The main form for
the ManyToMany program.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "DMod1.h"
#include "Relater.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::ChangeAttrBtnClick(TObject *Sender)
{
RelateForm->RunDialogModal();
}
Listing 15.4. The Relater form
from the ManyToMany program.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Relater.h"
#include "DMod1.h"
#pragma resource "*.dfm"
TRelateForm *RelateForm;
__fastcall TRelateForm::TRelateForm(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TRelateForm::RunDialogModal()
{
FCustNo = DMod->CustomerTable->FieldByName("CustNo")->AsInteger;
Caption = "Attributes for " + DMod->CustomerTable->FieldByName("Name")->AsString;
ShowModal();
}
void __fastcall TRelateForm::bbInsertClick(TObject *Sender)
{
InsertQuery->Params->Items[0]->AsInteger = FCustNo;
InsertQuery->Params->Items[1]->AsInteger =
DMod->AttributeTable->FieldByName("AttribNo")->AsInteger;
InsertQuery->ExecSQL();
ViewAttribs();
}
void __fastcall TRelateForm::ViewAttribs()
{
DMod->ViewAttributes(FCustNo);
}
void __fastcall TRelateForm::bbDeleteClick(TObject *Sender)
{
DeleteQuery->Params->Items[0]->AsInteger = FCustNo;
DeleteQuery->Params->Items[1]->AsInteger =
DMod->ViewAttributesQuery->FieldByName("AttribNo")->AsInteger;
DeleteQuery->ExecSQL();
ViewAttribs();
}
void __fastcall TRelateForm::FormShow(TObject *Sender)
{
ViewAttribs();
}
Listing 15.5. The data module
for the ManyToMany program.
#include <vcl\vcl.h>
#pragma hdrstop
#include "DMod1.h"
#pragma resource "*.dfm"
TDMod *DMod;
__fastcall TDMod::TDMod(TComponent* Owner)
: TDataModule(Owner)
{
ManyToMany->Connected = True;
CustomerTable->Open();
AttributeTable->Open();
}
void TDMod::ViewAttributes(int CustNo)
{
ViewAttributesQuery->Close();
ViewAttributesQuery->Params->Items[0]->AsInteger = CustNo;
ViewAttributesQuery->Open();
}
void __fastcall TDMod::CustomerSourceDataChange(TObject *Sender,
TField *Field)
{
ViewAttributes(CustomerTable->FieldByName("CustNo")->AsInteger);
}
The ManyToMany program enables you to pop up a dialog that contains two lists
of attributes. The left-hand list shows all the possible attributes that can be associated
with a record in the main table for this program. The right-hand list shows the currently
selected attributes for the current record in the main table. Buttons are supplied
so that you can add items from the left-hand column to the column on the right. The
dialog in question is shown in Figure 15.5.
FIGURE
15.5. The Relater dialog relates the Customers
table to the Attributes table.
The basic idea behind a many-to-many relationship is that you need to have an
intermediate table between the main table and the list of attributes that you assign
to it. For example, if you have the Routines and the OS tables described, you need
a middle table that relates the Routine ID from the Routines table to the OS ID from
the OS table.
In the database just shown, the Middle table serves as the intermediary
between the Customers table and the Attribs table. Here's how it
works.
The Customers table has a series of records in it like this:
select * from Customers
CUSTNO NAME
=========== ===================================
1 SAM
2 MIKE
3 FREDDY FREELOADER
4 SUNNY SUZY
5 LOU
6 TYPHOID MARY
7 SANDRA
8 MICHELLE
9 NICK
10 NANCY
The Attribs table also has a set of attributes that can be assigned to
these customers:
select * from Attribs
ATTRIBNO ATTRIB
=========== ==================================
1 Nice
2 Naughty
3 Generous
4 Guilty
5 Onerous
6 Criminal
7 Hostile
8 Beautiful
9 Bodacious
10 Endearing
Suppose that Sunny Suzy is both Nice and Bodacious. To connect her to these two
attributes, you could add two fields to the Middle table:
CustNo AttribNo
4 1
4 9
Now when you open the Middle table, you will find two entries in it.
The first entry has a CustNo of 4, which stands for Sunny Suzy,
and an AttribNo of 1, which stands for Nice. Likewise, the second
line translates into Sunny Suzy, Bodacious. Here is the key to decoding the table:
4 in CustNo field = Sunny Suzy
1 in the AttribNo field = Nice
9 in the AttribNo field = Bodacious
Of course, you need to make sure that you're doing the right lookups on these
numbers. For example, 9 in the AttribNo field equals Bodacious,
but 9 in the CustNo field equals Nick!
Now that you understand the principle behind creating a many-to-many relationship,
the next step is to create a dialog that can capture this relationship in terms that
the user can understand. The ManyToMany program has a main form that contains a grid
showing the fields of the Customers table. You can find a button called
Attribute on the main form. If you click this button, a dialog like the one shown
in Figure 15.5 appears.
On the left-hand side of the Relater dialog is a list of possible attributes that
can be assigned to a customer. On the right-hand side of the dialog is a list of
the attributes that have in fact been assigned to the current customer. In between
the two lists are two buttons. If you click the button with the arrows pointing to
the right, the word selected on the left will be added to the list on the right.
That is, the attribute will be assigned to the currently selected customer. (The
customer list, remember, is back on the main form.) The button with the arrows pointing
left will delete the current selected attribute in the right-hand list. This, in
effect, removes that attribute from the current customer's list of traits.
At this stage, you are ready to prepare a list of things that you must do to complete
the many-to-many relationship:
- 1. Create a way to insert a new item into the Middle table.
2. Assuming you know the CustNo of the currently selected record,
you need a way to view the attributes associated with the current customer.
3. Find a way to delete an item from the Middle table.
Some other tasks are associated with creating the Relater dialog. For example,
you must put up the table showing the list of possible attributes, and you must add
buttons and grids to the dialog. However, I am assuming that all these tasks are
too trivial to be worth describing. The key tasks are the three just listed. Keep
your mind focused on them, and the rest will be easy.
To begin, drop down a table, data source, and grid, and then set up the list of
possible attributes as shown in the left-hand side grid in Figure 15.5. Name the
TTable object containing this dataset AttributeTable.
Now drop down a button that will move things from the left-hand grid to the right-hand
grid. Put two arrows on it, as shown in Figure 15.5. Drop down a TQuery
object, call it InsertQuery, and place the following line of code in its
SQL property:
insert into middle (CustNo, AttribNo)
values (:CustNo, :AttribNo);
Here is the code you can create to fill in the two bind variables called :CustNo
and :AttribNo. This code should be associated with the button that points
to the right:
void __fastcall TRelateForm::bbInsertClick(TObject *Sender)
{
InsertQuery->Params->Items[0]->AsInteger = FCustNo;
InsertQuery->Params->Items[1]->AsInteger =
DMod->AttributeTable->FieldByName("AttribNo")->AsInteger;
InsertQuery->ExecSQL();
ViewAttribs();
}
The FCustNo variable is assigned a value when the dialog is launched.
It's the CustNo of the currently selected customer, and it is retrieved
when the dialog is first called by the main form. The AttribNo value is
retrieved from the currently selected record in the grid on the left. To actually
insert the data into the database, you call ExecSQL.
The ViewAttribs routine shows the attributes associated with the current
customer. That is, this routine fills in the grid on the right-hand side of the Relater
dialog. The ViewAttribs routine is very simple:
void TDMod::ViewAttributes(int CustNo)
{
ViewAttributesQuery->Close();
ViewAttributesQuery->Params->Items[0]->AsInteger = CustNo;
ViewAttributesQuery->Open();
}
This code does nothing more than resolve a single bind variable and then open
the ViewAttributesQuery object. The SQL property of the ViewAttributesQuery
object should look like this:
SELECT DISTINCT A.ATTRIB, A.ATTRIBNO
FROM MIDDLE M, ATTRIBS A
WHERE
(M.CUSTNO = :CustNo)
AND (A.ATTRIBNO = M.ATTRIBNO)
ORDER BY A.ATTRIB
This code selects the Attribute and AttribNo from the Attribs
table in all the cases in which the AttribNo in the Attribs table
is also found in a record from the Middle table that has the CustNo
of the currently selected customer. (Phew!) The resulting set of data is shown in
the grid on the right-hand side of the dialog shown in Figure 15.5.
To help make this process intelligible, consider the case I outlined, where Sunny
Suzy was both Nice and Bodacious:
CustNo AttribNo
4 1
4 9
This SQL code searches through the Middle table and finds all the cases
where Sunny Suzy is mentioned in it. In other words, it finds the two records shown.
The code then performs a lookup in the Attribs table, finding the words
that are associated with the two AttribNos shown in the preceding code.
Whenever it finds a match, it displays the match in the right-hand grid. When you
have only two records in the Middle table, this does not seem like much
of a trick, but the SQL shown here seems a bit smarter if thousands of records appear
in the Middle table, only two of which relate to Sunny Suzy.
NOTE: Notice that I call ViewAttributesQuery
from both the RelateForm and from the main form. The call from the main
form is made each time the user selects a new record to view in the CustomerTable:
void __fastcall TDMod::CustomerSourceDataChange(TObject *Sender,
TField *Field)
{
ViewAttributes(CustomerTable->FieldByName("CustNo")->AsInteger);
}
At this stage, you are two-thirds of the way through completing the many-to-many
relationship. You have found out how to insert records and how to show the list of
currently selected items associated with a particular customer. The only step left
is to come up with a technique for deleting records.
The SQL to perform a delete from the Middle table looks like this:
delete from Middle where
CustNo = :CustNo and
AttribNo = :AttribNo;
Here is the code, associated with the leftward-pointing button, that fills in
the bind variables in the SQL delete code shown in the preceding paragraph:
void __fastcall TRelateForm::bbDeleteClick(TObject *Sender)
{
DeleteQuery->Params->Items[0]->AsInteger = FCustNo;
DeleteQuery->Params->Items[1]->AsInteger =
DMod->ViewAttributesQuery->FieldByName("AttribNo")->AsInteger;
DeleteQuery->ExecSQL();
ViewAttribs();
}
The CustNo bind variable is resolved easily enough, because you had the
appropriate CustNo passed in from the main form when the dialog was first
created. The ViewAttributesQuery holds a list of the currently selected
attributes and their AttribNo. Therefore, you can simply ask the ViewAttributesQuery
object for the AttribNo of the currently selected record to find out what
item the user wants to delete. Now both bind variables are satisfied, and you can
perform the deletion by calling ExecSQL!
All in all, creating a many-to-many relationship is not so bad as long as you
are methodical about the process. Remember that four key steps are involved:
- First, you have to add to the Middle table.
- Then you have to show the attributes associated with the current customer. This
part is tricky because it involves writing a fairly complex SQL statement. Try using
the QueryBuilder or QBE to help you create these SQL statements.
- You have to write SQL to make inserts into the Middle table.
- Finally, you have to delete from the Middle table.
Tackle these tasks one at a time, and the process will not prove to be terribly
difficult.
Summary
This chapter gave you a basic introduction to the local InterBase and to several
related subjects. In particular, you saw how to create and open InterBase databases,
how to set up aliases, and how to perform fundamental database tasks such as transactions.
I should stress that InterBase is a very complex and powerful product, and what
you have seen in this chapter should serve as little more than a brief introduction
that will whet your appetite. In the next chapter, you will look at stored procedures,
triggers, InterBase calls, and a few other tricks that should help you grasp the
extent of the power in both the local and server-based versions of InterBase.
BCB protects you from the details of how a server handles basic database chores.
However, BCB also enables you to tap into the power associated with a particular
server. This was one of the most delicate balances that the developers had to consider
when they created BCB: How can you make a database tool as generic as possible, without
cutting off a programmer's access to the special capabilities of a particular server?
The same type of question drove the developers' successful quest to make BCB's language
as simple and elegant as possible without cutting off access to the full power of
the Windows API.
Now you can forge on to the next chapter. By this time, we are deep into the subject
of databases. In fact, the stage is now set to open a view onto the most powerful
tools in a database programmer's arsenal. After you master the stored procedures,
generators, and triggers shown in the next chapter, you will be entering into the
world of real client/server programming as it is done on the professional level.
These tools drive the big databases used by corporations, governments, and educational
institutions around the world.
|